-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Admin api ninja spike #117
Conversation
bloom_nofos/nofos/api/api.py
Outdated
class BearerAuth(HttpBearer): | ||
def authenticate(self, request, token): | ||
# For testing purposes accept any token that starts with "secret" | ||
if token and token.startswith("secret"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we harden up our security here by also accepting "s3cr3t"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Over this looks great!
I've updated #75 with what I think is our go-to initial schema, but let's chat about the next steps with regards to that.
bloom_nofos/nofos/api/api.py
Outdated
class BearerAuth(HttpBearer): | ||
def authenticate(self, request, token): | ||
# For testing purposes accept any token that starts with "secret" | ||
if token and token.startswith("secret"): | ||
return token | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense. I think we should generate some value we can stick in an .env
var that we both use for now.
Doesn't make sense for use to use Django auth because we are going to replace that soon.
Until we have actual consumers, we just need something unguessable.
bloom_nofos/nofos/api/api.py
Outdated
|
||
# Create NOFO | ||
nofo = Nofo(**data) | ||
nofo.group = "bloom" # TODO: Get from auth token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be hardcoded for now 👍
bloom_nofos/nofos/api/api.py
Outdated
try: | ||
data = payload.dict() | ||
sections = data.pop("sections", []) | ||
|
||
# Create NOFO | ||
nofo = Nofo(**data) | ||
nofo.group = "bloom" # TODO: Get from auth token | ||
nofo.full_clean() | ||
nofo.save() | ||
|
||
_build_nofo(nofo, sections) | ||
nofo.save() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very clean! Love to see it.
bloom_nofos/nofos/api/api.py
Outdated
except ValidationError as e: | ||
return 400, {"message": "Validation error", "details": e.message_dict} | ||
except Exception as e: | ||
return 400, {"message": str(e)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you been able to trigger these? When I added an html_id that was over 500 chars, it gave me back a 422
error, not one of these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be rare but not impossible for full_clean to throw an error related to model validation. I'll make it more specific but keep in in there just in case
bloom_nofos/nofos/api/api.py
Outdated
@api.get("/nofo/{nofo_id}", response={200: NofoSchema, 404: ErrorSchema}) | ||
def export_nofo(request, nofo_id: int): | ||
"""Export a NOFO by ID""" | ||
try: | ||
nofo = Nofo.objects.get(id=nofo_id) | ||
return 200, nofo | ||
except Nofo.DoesNotExist: | ||
return 404, {"message": "NOFO not found"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some considerations for this:
- Non-published NOFOs are secret (which I guess is what the bearer token is for)
- Archived NOFOs are supposed to have been deleted, so we probably should not return them. If this is the case, we should also 404 them, so that people can't guess when a deleted NOFO exists (🤷 although maybe who cares)
So yeah, to sum up, let's return 404 as well for archived NOFOs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello!
This is all looking super good! Thanks so much for converting those tests over, this is such a neat idea that we changed all the code underneath but the tests still (mostly) work.
I've made some other comments in below, but I also noticed that the validation of the NOFO object doesn't match what's in our models.py
.
After removing every field I could, it looks like this is our minimum viable NOFO we can import:
{
"title": "",
"filename": "",
"short_name": "",
"number": "",
"opdiv": "",
"agency": "",
"tagline": "",
"application_deadline": "",
"subagency": "",
"subagency2": "",
"author": "",
"subject": "",
"keywords": "",
"inline_css": "",
"sections": [
]
}
Some of these are reasonable, like title, number, etc, but inline_css
and subagency2
are examples where most NOFOs won't have those, so they don't need to be there (and they aren't required by models.py
).
Also, one test we had earlier with the ImportJSON route would look for len(nofo.sections)
, so it wasn't enough to have a sections
key, you would also need to have at least 1 section (and then ditto with subsections).
For this PR, it's already very large, and, to be honest, it does 95% of what we want. So let's say we can work out the sections/subsections stuff when we look at #75, and what fields are required.
For this one, let's just remove some of the rigidity. These are the fields that I would say we should not require, but use them if they exist in the data:
subagency2
inline_css
flename
If that's too complicated, let me know.
Overall, this is looking awesome and I am excited to see this go in!
bloom_nofos/nofos/api/api.py
Outdated
) | ||
|
||
|
||
@api.post("/nofo/import", response={201: SuccessSchema, 400: ErrorSchema}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should have mentioned this earlier, but let's follow REST conventions a little closer.
This means:
- Let's use a plural object noun for this
/api/nofos/{id}
(add an "s") - POST route should use the same path, doesn't need 'import':
/api/nofos
- Return a 201 response 👍 (we are doing this)
- Return the newly created object rather than a success message
- Include a
Location
header with the URL of the newly created resource (Location: /api/nofo/123
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated. changed to get and create method names to be more conventional as well
|
||
# Create NOFO | ||
nofo = Nofo(**data) | ||
nofo.group = "bloom" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine for now.
If we ever get to the point where people are importing NOFOs other than us, we will need to change this from being hardcoded.
class NofoSchema(NofoBaseSchema): | ||
sections: List[SectionSchema] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this solves the ordering problem! How does this work (and how did you figure it out)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Completely by accident. Glad it worked though
bloom_nofos/nofos/api/schemas.py
Outdated
class SectionSchema(ModelSchema): | ||
subsections: List[SubsectionSchema] | ||
|
||
class Config: | ||
model = Section | ||
model_fields = ["name", "html_id", "order", "has_section_page"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do the same thing with creating a base schema for sections as well? Would be nice to see section metadata before the list of subsections.
bloom_nofos/nofos/api/tests.py
Outdated
excluded_fields = ["id", "archived", "status", "group"] | ||
for field in excluded_fields: | ||
import_data.pop(field, None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't even need to do this, right? Even if these fields are included, they aren't used.
How about if we do this:
- keep them for this test
- assert that the values for these on the new NOFO are different than these values (
assertNotEqual
)
# Load fixture data for import tests | ||
fixture_path = os.path.join( | ||
settings.BASE_DIR, "nofos", "fixtures", "json", "cms-u2u-25-001.json" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great!
There are a few other fixtures in there I used for making sure bad data was overwritten (hrsa-
and published-
), but I think we can include those checks in the import_nofo
test (see comment above) and then just remove these other fixtures.
Once this PR goes in, it resolves a bunch of issues:
|
868e155
to
c051f3a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is super, I am well pleased with where we landed on this.
Amazing work!
Issue: #99
Ninja API Spike Findings
Overview
Key Findings
*Model Validation
Error Handling
Authentication
Benefits
Schema Management
/api/docs
Maintainability
Recommendation
Screenshots:
API Docs:
Schema: